# --------------------------------------------------------------------------------
#  NoiseTube Web application
#  
#  Copyright (C) 2008-2010 SONY Computer Science Laboratory Paris
#  Portions contributed by Vrije Universiteit Brussel (BrusSense team), 2008-2011
# --------------------------------------------------------------------------------
#  This library is free software; you can redistribute it and/or modify it under
#  the terms of the GNU Lesser General Public License, version 2.1, as published
#  by the Free Software Foundation.
#  
#  This library is distributed in the hope that it will be useful, but WITHOUT
#  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
#  FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
#  details.
#  
#  You should have received a copy of the GNU Lesser General Public License along
#  with this library; if not, write to:
#    Free Software Foundation, Inc.,
#    51 Franklin Street, Fifth Floor,
#    Boston, MA  02110-1301, USA.
#  
#  Full GNU LGPL v2.1 text: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt
#  NoiseTube project source code repository: http://code.google.com/p/noisetube
# --------------------------------------------------------------------------------
#  More information:
#   - NoiseTube project website: http://www.noisetube.net
#   - Sony Computer Science Laboratory Paris: http://csl.sony.fr
#   - VUB BrusSense team: http://www.brussense.be
# --------------------------------------------------------------------------------
 
require 'builder'
require 'gchart'
require 'shapefile'
require 'maps/process_maps'

class SearchException < Exception;
end

class ApiController < ApplicationController

  #fixes bug that doesn't call api methood
  include ApiHelper

  before_filter :verify_apikey, :only => [:newsession, :update, :upload, :postlog, :endsession, :resumesession, :endallsessions, :search, :traces, :aggmap, :trackmap]

  def index
    redirect_to '/api_overview'
  end
  
  def mobilecalibrations
    #Contains hack to deliver alternative calibration file to Android clients with version < v1.2.3 (this file uses the Build.BRAND field for deviceBrand instead of Build.MANUFACTURER)
    androidAgentSlash = 'NoiseTubeMobileAndroid/'
    agent = request.headers['User-Agent']
    if !agent.nil? && agent[0, androidAgentSlash.length] == androidAgentSlash
        androidClientVersion = agent[androidAgentSlash.length, agent.length]
    	if androidClientVersion <= "v1.2.2"
    	  send_file "#{Rails.root}/public/calibrationsAndroidPre123.xml", :type=>"application/xml"
    	else
    	  send_file "#{Rails.root}/public/calibrations.xml", :type=>"application/xml"
    	end
    else
    	send_file "#{Rails.root}/public/calibrations.xml", :type=>"application/xml"
    end
  end

  # Mobile Authentication
  def authenticate
    user = User.find_by_login(params[:login])
    if user.nil? || !user.authenticated?(params[:password])
      render :json => "error"
    else
      render :json=> user.salt
    end
  end

  # Ping tool
  def ping
    render :text => "ok", :status=>200
  end


  # Start a new measurement session #
  def newsession
    # in case we haven't closed the previous session
    # close_session  # closes the most recent running session
    
    # create the new track
    track = start_session
    render :text => "ok #{track.id}", :status=>200
  end


  # close a measurement session  #
  def endsession
    track = running_track(false) #do not start a new one
    if close_session(track)
      render :text => "ok", :status=>200
    else
      render :nothing => true, :status=>404 #specified track did no exist, or it was already closed, or there is no running track
    end
  end

  def endallsessions
    #TODO
  end

  def resumesession
    track = running_track(false) #do not start a new one
    unless track.nil?
      render :text => "ok #{track.id}", :status=>200
    else
      render :nothing => true, :status=>404 #specified track did no exist of there is no running track
    end
  end

  # ## Update the sensor request: http://noisetube.net/api/update?time=(timestamp)&db=(decibel)&l={geo:(lat,lon)|placeid}
  # Parameters:
  #   time={time}: the time of the measure (in second) (required)
  #   db=: decibel in db(A) integer (required)
  #   l: geo:{lat,lng}|placeID
  #   placeID= the id of a place e.g. one of your favoraite location (home, office) , the station of a subway
  #   geo coordinates: l=geo:lat,lng
  #   result: response 200 : ok response 500 : ##
  def update
    begin
      track = running_track(true) #start a new one if needed
      track.add_measurement(params)
      #render :nothing=> true
      render :text => "ok", :status=>200
    rescue Exception

      render :text=> $!.message + "<br/>" +$!.backtrace.join("</br>")
    end
  end

  # Update with tag interval information
  def tagintervals
    tagintervals = params[:tagintervals]

    unless tagintervals.blank?
      #todo: add clientsession and clientid in measures table

      MeasurementsTag.addTagInterval(from, to, sessionid) # in the format: from>id>=to
    end

  end


  # Update the sensor request: http://noisetube.net/api/upload # batch version (through JSON format)
  #{
  #    "key" : "2812f53fdad045089a1ef1614d3a70a7cae84871",
  #    "measure" : [
  #                {
  #                    "db" : "26",
  #                "l" : "geo:50.821452,4.396129",
  #                "time``" : "2013-03-25T16:40:16Z+01:00"
  #            },
  #                {
  #                    "db" : "27",
  #                "l" : "geo:50.821452,4.396129",
  #                "time" : "2013-03-25T16:40:18Z+01:00"
  #            },...
  #            ]}

  def upload
    track = running_track(true) #start a new one if needed, todo: link this to the user
    begin
    params[:measures].each { |m_params|  
      unless m_params.nil?
        #old JSON format (usertags & autotags together)
        time = m_params[:time]  || m_params[0]
        db = m_params[:db]  || m_params[1]
        l = m_params[:l]  || m_params[2]
        tag = m_params[:tag]  || m_params[3]
        #removed autotags as param[4]

        track.add_measurement({
                  :time=>time,
                  :db=>db,
                  :l=>l,
                  :tag=>tag})
      end
    }
    render :text => "ok #{track.id}", :status=>200

    rescue => ex
      logger.error ex.message
      render :text => "error", :status=>200
    end
  end

  def postlog
    directory = "log/posted"
    # create the file path
    path = File.join(directory, "User#{@user.id}.log")
    # write the file
    File.open(path, 'a') { |f| f.write(params[:log]) }
    render :text => "ok", :status=>200
  end

  # search measures
  def search
    begin
      measures = Measure.search(params, [:user], (user_iscopeadmin? @user))
      temp = measures.collect { |measure|
        {:lat=>measure.geom.try(:lat),
         :lng=>measure.geom.try(:lng),
         :made_at=>measure.made_at,
         :loudness=>measure.loudness,
         #:tags=>measure.tags, #huge performance hit   todo: workaround
         :user=>measure.user.try(:id)}
      }

      #get a bunched-up list of tags

      # send
      render :json=>temp.to_json

    rescue SearchException
      render :text=>"#{$!.message}"
    end
  end


  # ## Tag distribution ##
  def tagdist

    # querying
    labels, freq=Api.distribution_tags(params)

    respond_to do |format|
      format.png{
        graph=@template.generate_tags_distribution_graph(labels,freq)
        redirect_to graph
      }

      format.json{
        render :json=>[labels, freq].to_json
      }
    end
  end

  def leqdist
    dist=Api.distribution_leq(params)
    
    respond_to do |format|
      # JSON RESPONSE
      format.json{
        dist_label={}
        dist.each_with_index{ |e, i|  dist_label[Measure::DISTRIBUTION_LABEL[i]]=e}
        render :json=>dist_label.to_json
      }

      # PNG RESPONSE
      format.png{
        graph= view_context.generate_leq_distribution_graph(dist,params)
        redirect_to graph
      }
    end
  end


  #for traces
  def traces
    #get the params
    #api?request=mytraces&key=mykey&track=mytrack
    track = params[:track]
    unless track.nil?
      redirect_to "/users/#{@user.id}/tracks/#{track}.json"
    else
      render :text=> "Track for the user not found"
    end
  end

  def aggregate_test

    begin
      ms = Track.find(4421).measures.all
      #remove gems of nil
      #ms = m.select { |i| !i.geom.nil? }
      bbm = ProcessMaps.boundingbox(ms)
      bbmg = ProcessMaps.create_grids(0.01,0.01,bbm)
      bbmgd =  ProcessMaps.distribute_measures(ms, bbmg,30)
      bbstats = ProcessMaps.calculate_statistics(bbmgd)
      render :json=> bbstats.as_json, :status => "200 OK"
      rescue Exception

      render :text=> $!.message + "<br/>" +$!.backtrace.join("</br>")
    end

  end

  # aggmap - serves aggregate map. restricted bounding box for faster processing
  #param key: api key
  #param box: coordinates (lat1,lng1,lat2,lng2)
  #param since: date from (YYYY-mm-dd)
  #param to: date to
  #param mapname: mapname
  #param map: the id of the map to retrieve. if processed the map will be returned as shapefile. if not a status 503 is returned
  def aggmap

    #check if params[:mapid] is set
    if params[:map]

      map = AggregateMap.find(params[:map])
      if map and map.generated == true and map.comments != "Processing incomplete"
        #map has been processed. send shapefile
        map = AggregateMap.find(params[:map])
        filename = SHAPE_FILES_PATH + Shapefile::create_filename_for_map(map) + ".zip"
        if File.exist? filename
          return_status =  { json: {status: '200', message: 'map processing completed successfully'}.to_json, status: 200}
        else
          return_status =  { json: {status: '200', message: 'no measures found for map'}.to_json, status: 200}
        end
      else
        #send not complete
        return_status =  { json: {status: '200', message: 'map processing incomplete'}.to_json, status: 200}
      end
    else

      #new request or aggregate map
      #defaults
      start_time = params[:since]
      end_time = params[:until]
      #times
      start_time = DateTime.new(2000,1,1).strftime("%Y-%m-%dT%H:%M") if start_time.blank?
      end_time = DateTime.now.strftime("%Y-%m-%dT%H:%M") if end_time.blank?

      #process the times
      from = parse_date_with_optional_time start_time
      to = parse_date_with_optional_time end_time

      #validation
      if to < from
        return_status = { text: "Times specified are invalid", status: 400}
      else


        #iscope admins can request maps with longer duration
        #unless iscopeadmin?
          #if ((map.end_time - map.start_time) / 1.day) > 358
            #return_status = { text: "Times specified exceed range", status: 400}
          #end
        #end

        #bounds as points
        coords = params[:box].split(",")
        params[:geotl] = "#{coords[0]},#{coords[1]}"
        params[:geobr] = "#{coords[2]},#{coords[3]}"

        map = generate_aggregate_map(params, from, to)    #calls method in api_helper

        return_status = { json: map.as_json(:only => [:id, :name]), status: 200 }
     
      end
    end

    render return_status
  end

  # Performance: Return only 1 measurement per +- 10 meter to just show the iScope city administrators where data is available
  #
  # SELECT DISTINCT on (round(ST_X(geom)::numeric, 4), round(ST_Y(geom)::numeric, 4)) id, created_at, loudness, track_id, user_id, location, round(ST_X(geom)::numeric, 4), round(ST_Y(geom)::numeric, 4)
  #     loudness_index, corrected, corrected_distance, segment_id, peak,
  #     longexposure, temperature, tagged, time_index, made_at
  # FROM measures
  # where geom is not null
  # and measures.geom && ST_MakeEnvelope(45.81327495304739, 15.971490753868883, 15.997342864921276, 45.80304487112522, 4326)


  # ------------------------------------------------------------------
  private #!!!

  # verify the API Key
  def verify_apikey
    @user=User.find_by_salt(params[:key])
    if @user.nil?
      render(:text=> "API key not found", status: 400)
    end
  end

  def most_recent_running_track
    @user.tracks.find(:last, :conditions=>"ends_at IS NULL", :order=>"starts_at asc")
  end

  # get specified or most recent running track (or a new one if allowed)
  def running_track(start_new_if_none=false)
    track = @user.tracks.find_by_id(params[:track]) unless params[:track].blank? #try to find track by id (if id was specified)
    track = most_recent_running_track if (track.nil? or not track.ends_at.nil?) #no specified track (or it does not exist, or is already closed) --> try most recent running track
    track = start_session if (track.nil? and start_new_if_none) #or start a new one if permitted
    track
  end

  def close_session(track=most_recent_running_track)
    unless track.nil?
      last_measurement = track.measures.find(:last, :order=>"made_at asc")
      unless last_measurement.nil?
        track.ends_at = last_measurement.made_at
        track.save
      else
        track.destroy  #no last measurement --> no measurements at all --> delete empty track
      end
      return true
    else
      return false
    end
  end

  # create a new session for a given user
  def start_session
    start_time = Time.now #default
    start_time = DateTime.parse(params[:starts_at]) unless params[:starts_at].blank?
    track = Track.create(:user=>@user)
    track.starts_at = start_time #will be overwritten with timestamp (made_at) of first measurement during postprocessing
    track.client = params[:client]
    track.client_version = params[:clientversion]
    track.device = "#{params[:devicebrand]} #{params[:devicemodel]}"
    # TODO...
    track.save
    return track
  end

  def parse_date_with_optional_time(datestr)
    if datestr !~ /T/i
      DateTime.strptime(datestr, "%Y-%m-%d") rescue nil
    else
      DateTime.strptime(datestr, "%Y-%m-%dT%H:%M") rescue nil
    end

  end

end
